// Copyright 2012 Jay Young. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview
 *
 * Provides a class whose job is to parse type expressions and return an HTML
 * string including links to registered type symbols.
 */

var commonTemplate = require('rdoc/templates/common'),
    logger = require('ringo/logging').getLogger('TypeLinker');

/**
 * A class whose job is to parse type expressions and return an HTML string
 * that includes links to registered type symbols.  Also performs simple
 * operations on common expressions such as collapsing null union types to '?'
 * and optional arguments to '='.
 *
 * @param {!Object.<string, DocMap.Entry>}
 * @constructor
 */
var TypeLinker = exports.TypeLinker = function(types) {

  /**
   * Cache this here.  We'll be accessing this object a LOT.
   * @type {!Object.<string, DocMap.Entry>}
   * @private
   */
  this.types_ = types;
};


/**
 * Mapping of primitive type strings to their representation.
 * @enum {string}
 */
TypeLinker.Primitive = {
  'boolean': 'boolean',
  'string': 'string',
  'number': 'number',
  'null': 'null',
  'undefined': 'undefined',

  '<Any Type>': '*',
  '...<Any Type>': '...*'
};


/**
 * Process a type expression, taking the type string and returning an HTML
 * string containing links to registered types.
 *
 * @param {string} type The type string to be parsed.
 * @param {boolean=} isParam Whether or not this is a function parameter.
 *    Defaults to "false".
 * @return {string} An HTML type string with links to doc pages for any included
 *    registered types.
 */
TypeLinker.prototype.processTypeExpr = function(type, isParam) {
  if (TypeLinker.Primitive[type]) {
    return TypeLinker.Primitive[type];
  }
  
  if (Object.prototype.hasOwnProperty.call(this.types_, type)) {
    return this.typeLink(type);
  }

  if (type.lastIndexOf('function', 0) === 0) {
    return this.processFunctionType(type);
  }

  if (type.lastIndexOf('Object.<', 0) === 0 ||
      type.lastIndexOf('Array.<', 0) === 0) {

    try {
      return this.processParameterizedType(type);
    }
    catch (e) {
      e.message += '\n : ' + type
      throw e;
    }
  }
  
  if (type.lastIndexOf('...', 0) === 0) {
    return '...' + this.processTypeExpr(type.slice(3), isParam);
  }
  
  if (type.charAt(0) === '(') {
    return this.processUnionType(type, isParam);
  }

  if (type.charAt(0) === '{') {
    return this.processRecordType(type);
  }

  return type;
};


/**
 * Process a type expression which is a parameterized object or array type.
 *
 * @param {string} type The type string to be parsed.
 * @return {string} An HTML string with links to doc pages for any included
 *    registered types.
 */
TypeLinker.prototype.processParameterizedType = function(type) {
  if (type.charAt(type.length - 1) !== '>') {
   throw Error('Parameterized type must end in ">" : ' + type);
  }

  var start = 0, end = type.length - 1, // type.substring() indexes
      isObject;
  if (type.lastIndexOf('Object.<', 0) === 0) {
    isObject = true;
    start = 8;
  }
  else if (type.lastIndexOf('Array.<', 0) === 0) {
    isObject = false;
    start = 7;
  }
  else {
    throw Error('Parameterized type must begin with "Object.<" or "Array.<"' +
                ' : ' + type);
  }

  var inner = type.substring(start, end);
  
  // Structure of "inner" could be either a single value type or comma-
  // separated index & value types made up of primitive types, union types,
  // record types, other parameterized types, or some combination to
  // arbitrarily nested depths.

  // First, we have to find out whether we have one or two parameter types.
  // Every time we enter a union type or another parameterized type, increase
  // depth.  If we find a ',' at depth 0, we have two types.

  var depth = 0,
      commaAt = -1;
  for (var i = 0, len = inner.length; i < len; ++i) {
    var curr = inner.charAt(i);
    if (curr === '(' || curr === '{' || curr === '<') {
      ++depth;
    }
    else if (curr === ')' || curr === '}' || curr === '>') {
      --depth;
    }
    else if (curr === ',' && depth === 0) {
      if (commaAt > -1) {
        throw Error('Mal-formed type (too many commas): ' + type);
      }
      else {
        commaAt = i;
      }
    }
  }

  if (depth !== 0) {
    throw Error('Nested record, union, or parameter types not properly closed');
  }

  if (commaAt > -1 && !isObject) {
    throw Error('Parameterized array should not have index type: ' + type);
  }

  var composed;
  if (commaAt > -1) {
    var indexType = inner.substr(0, commaAt).trim(),
        valueType = inner.substr(commaAt + 1).trim();

    composed = 'Object.&lt;' +
                  this.processTypeExpr(indexType) + ', ' +
                  this.processTypeExpr(valueType) +
               '&gt;';

  }
  else if (isObject) {
    composed = 'Object.&lt;' + this.processTypeExpr(inner) + '&gt;';
  }
  else {
    var composed = 'Array.&lt;' + this.processTypeExpr(inner) + '&gt;';
  }
  return composed;
};


/**
 * Process a union type.
 *
 * @param {string} type The type string to be parsed.
 * @param {boolean=} isParam Whether or not this is a function parameter.
 * @return {string} An HTML string with links to doc pages for any included
 *    registered types.
 */
TypeLinker.prototype.processUnionType = function(type, isParam) {
  if (type.charAt(0) !== '(' || type.charAt(type.length - 1) !== ')') {
    throw Error('This is not a union type: ' + type);
  }

  var bars = [], // index of top-level '|' characters
      depth = 0;
  for (var i = 1, len = type.length - 1; i < len; ++i) { // Ignore outer ( )
    var curr = type.charAt(i);
    if (curr === '(' || curr === '{' || curr === '<') {
      ++depth;
    }
    else if (curr === ')' || curr === '}' || curr === '>') {
      --depth;
    }
    else if (curr === '|' && depth === 0) {
      bars.push(i);
    }
  }

  if (bars.length === 0) {
    return type.substring(1, type.length - 1);
  }
  else {
    var start, end,
        subtypes = [];
    for (i = 0, len = bars.length; i <= len; ++i) {
      var subtype;
      if (i === 0) { // First one
        start = 1
        if (i === len) { // If this is the last subtype
          end = type.length - 1;
        }
        else {
          end = bars[i];
        }
      }
      else {
        start = end + 1;
        if (i === len) {
          end = type.length - 1;
        }
        else {
          end = bars[i];
        }
      }

      subtype = type.substring(start, end);
      subtypes.push(this.processTypeExpr(subtype));
    }

    // Process special cases:
    // (type|null) === ?type
    // @param {type|undefined} === type=
    // @param {type|null|undefined} === ?type=

    len = subtypes.length;
    var nullI = subtypes.indexOf('null'),
        undefI = subtypes.indexOf('undefined');
    if (len === 2 && nullI > -1) {
      var theType = nullI === 0 ? subtypes[1] : subtypes[0];
      return '?' + theType;
    }
    else if (len === 2 && undefI > -1 && isParam) {
      var theType = undefI === 0 ? subtypes[1] : subtypes[0];
      return theType + '=';
    }
    else if (len === 3 && undefI > -1 && nullI > -1 && isParam) {
      var i = 3 - undefI - nullI,
          theType = subtypes[i];
      return '?' + theType + '=';
    }
    
    return '(' + subtypes.join('|') + ')';
  }
};


/**
 * RegExp for matching function types.
 *
 * @type {RegExp}
 * @private
 */
TypeLinker.funcRX_ = /^function \((.*)\): (.*)$/;


/**
 * Process a function type.
 *
 * @param {string} type The function type expression.
 * @return {string} An HTML string containing links to any registered type doc
 *    pages.
 */
TypeLinker.prototype.processFunctionType = function(type) {
  var funcParts = type.match(TypeLinker.funcRX_);
  if (funcParts === null) {
    // Pre-processed function results, such as one pulled from a super-type,
    // will not properly match the regexp.

    return type;
  }
  
  var paramTypes = funcParts[1].split(','),
      returnType = funcParts[2];

  var params = [];
  for (var i = 0, len = paramTypes.length; i < len; ++i) {
    params[i] = this.processTypeExpr(paramTypes[i]);
  }

  var returned = this.processTypeExpr(returnType);

  return 'function (' + params.join(', ') + ') &#8658; ' + returned;
};


/**
 * Processes a record type, returning an HTML string with embedded links for
 * registered types.
 *
 * @param {string} type The type string to be processed.
 * @return {string} An HTML string containing links to any registered type doc
 *    pages.
 */
TypeLinker.prototype.processRecordType = function(type) {
  // Parse out the individual property:type pairs
  //
  // In case of other embedded record or parameterized types, we need to make
  // sure we only split on top-level commas.

  var commas = [], // index of top-level ',' characters
      depth = -1;
  for (var i = 0, len = type.length - 1; i < len; ++i) {
    var curr = type.charAt(i);
    if (curr === '(' || curr === '{' || curr === '<') {
      ++depth;
    }
    else if (curr === ')' || curr === '}' || curr === '>') {
      --depth;
    }
    else if (curr === ',' && depth === 0) {
      commas.push(i);
    }
  }

  // 1. Split the type string based on the top-level ","s
  // 2. Process each property's type, recursively
  // 3. Stitch the property/type pairs back together into a new, linked
  //     HTML string.
  
  var pairs = commas.length + 1,
      start = 1, // Ignore opening '{'
      end = commas.length > 0 ? commas[0] : type.length - 1, // Ignore final "{"
      linkedPairs = [];

  for (i = 0; i < pairs; ++i) {
    var pair = type.substring(start, end),
        colonAt = pair.indexOf(':'),
        name = pair.substring(0, colonAt),
        propType = pair.substring(colonAt + 1, pair.length);

    start = commas[i] + 1;
    end = commas[i + 1] ? commas[i + 1] : type.length - 1;

    linkedPairs[i] = name + ': ' + this.processTypeExpr(propType);
    
    
  }
  
  var typeStr = '{' + linkedPairs.join(', ') + '}';
  
  return typeStr;
};


/**
 * Checks whether the given string is a registered type and, if so, generate
 * an HTML link to the doc page for that type to be included in doc page.
 * If the string is not a type, it will be returned without modification.
 *
 * @param {string} text The string to check and convert if it's a type.
 * @param {boolean=} opt_anchor If true, always generate an <a name="$type">
 *    tag.  href="..." will only be included if the type is a registered type.
 * @param {?string=} opt_type Text may be something like
 *    "namespace.Class#method".  in this case, compare the type name to this
 *    scoped type.  If they are the same, remove it so only the anchor remains.
 * @return {string} HTML link to the type if registered or anchor is true, or
 *    the plain text if not.
 */
TypeLinker.prototype.typeLink = function(text, opt_anchor, opt_type) {
  var isRegistered = Object.prototype.hasOwnProperty.call(this.types_, text),
      hashIndex = text.indexOf('#');
  if (opt_anchor || isRegistered || (hashIndex > -1)) {
    var templateData = {};

    if (hashIndex > -1) {
      var type = text.substr(0, hashIndex),
          member = text.substr(hashIndex + 1);

      if (type === opt_type) {
        templateData.type = text = '#' + member;
        // The scoped type is always a registered type.
        templateData.kind = this.types_[type].kind;
        templateData.href = '#' + member;
      }
      else {
        if (Object.prototype.hasOwnProperty.call(this.types_, type)) {
          templateData.href = type.replace(/\./g, '_'.toLowerCase()) +
                              '.html' + '#' + member;
          templateData.type = text;
          templateData.kind = this.types_[type].kind;
        }
        else {
          return text;
        }
      }
    }

    if (isRegistered) {
      templateData.type = text;
      templateData.href = text.replace(/\./g, '_').toLowerCase() + '.html';
      templateData.kind = this.types_[text].kind;
    }

    if (opt_anchor) {
      templateData.type = text;
      templateData.anchor = text;
    }

    return commonTemplate.typeLink(templateData);
  }
  else {
    return text;
  }
};